/***********************************************************************
 * Module:  GraphWorkArea.java
 * Author:  Nikola
 * Purpose: Defines the Class GraphWorkArea
 ***********************************************************************/

package genedit.view.graph;

import genedit.controller.ActiveConnecting;
import genedit.controller.ResizeState;
import genedit.controller.RotateState;
import genedit.controller.SelectionState;
import genedit.controller.State;
import genedit.controller.interfaceActions.PropertyManager;
import genedit.gui.GMainFrame;
import genedit.gui.GMainMenu;
import genedit.gui.GToolbar;
import genedit.model.Workspace;
import genedit.model.graph.GraphElement;
import genedit.model.graph.Link;
import genedit.view.graph.SelectionHandleHandler.Handle;

import java.awt.Adjustable;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.awt.event.WindowListener;
import java.awt.event.WindowStateListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.util.Iterator;

import javax.swing.JPanel;
import javax.swing.JScrollBar;

@SuppressWarnings("serial")
public class GraphWorkArea extends JPanel implements AdjustmentListener, Printable {
	protected static final int minScrollValue = -1000;
	protected static final int maxScrollValue = 1000;

	private GraphWorkArea.GraphController controller;

	private GraphDocumentView view;

	private SelectionHandleHandler handleHandler = new SelectionHandleHandler();

	private Handle selectedHandle = null;

	private AffineTransform transformation = new AffineTransform(); // tekuca
																	// transformaciona
																	// matrica

	private JScrollBar scrollVertical = null;
	private JScrollBar scrollHorizontal = null;

	// inicijalna pozicija skrol bara, vazna kod pomeranja skrol bara
	private int hScrollValue = 0;
	private int vScrollValue = 0;

	double translateX = 0;
	double translateY = 0;
	double scaling = 1;

	public double getScaling() {
		return scaling;
	}

	public void setScaling(double scaling) {
		if (scaling > 2) {
			this.scaling = 2;
			return;
		}
		if (scaling < 0.2) {
			this.scaling = 0.2;
			return;
		}
		this.scaling = scaling;
	}

	public double getTranslateX() {
		return translateX;
	}

	public void setTranslateX(double translateX) {
		this.translateX = translateX;
	}

	public double getTranslateY() {
		return translateY;
	}

	public void setTranslateY(double translateY) {
		this.translateY = translateY;
	}

	final static double translateFactor = 10;

	private Rectangle2D rect;

	/** @param view */
	public GraphWorkArea(GraphDocumentView view) {
		this.view = view;
		this.rect = new Rectangle2D.Double(0, 0, 0, 0);
		setLayout(new BorderLayout());
		scrollHorizontal = new JScrollBar(JScrollBar.HORIZONTAL, hScrollValue, 10, minScrollValue,
				maxScrollValue);
		scrollVertical = new JScrollBar(JScrollBar.VERTICAL, vScrollValue, 10, minScrollValue,
				maxScrollValue);
		add(scrollHorizontal, BorderLayout.SOUTH);
		add(scrollVertical, BorderLayout.EAST);

		controller = new GraphController();
		controller.setCurrentState(new SelectionState(this));

		scrollHorizontal.addAdjustmentListener(this);
		scrollVertical.addAdjustmentListener(this);
		scrollHorizontal.setUnitIncrement(10);
		scrollVertical.setUnitIncrement(10);

		setBackground(Color.WHITE);
		addMouseListener(controller);
		addMouseMotionListener(controller);
		addMouseWheelListener(controller);
		addKeyListener(controller);
	}

	/** @param g */
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		repaint();
		Graphics2D g2 = (Graphics2D) g;

		if ((Boolean) PropertyManager.getInstance().getProperty(PropertyManager.GridVisible)) {
			drawGrid(g2);
		}

		// sacuvava stroke - moguce da ce ovde biti potrebno sacuvavanje jos
		// nekih parametara za iscrtavanje
		Stroke strokeBackup = g2.getStroke();

		g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f));
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

		AffineTransform def = g2.getTransform();
		g2.transform(transformation);

		Iterator<ElementPainter> iter = view.getIteratorElementPainters();
		while (iter.hasNext()) {
			iter.next().paint(g2);
		}
		handleHandler.paintSelectionHandles(g2, view.getModel(), transformation.getScaleX());

		// iscrtavanje pravougaonika za lasso
		g2.setPaint(Color.BLACK);
		g2.setStroke(new BasicStroke((float) (1 / scaling), BasicStroke.CAP_SQUARE,
				BasicStroke.JOIN_BEVEL, 1f, new float[] { (float) (3 * (1 / scaling)),
						(float) (6 * (1 / scaling)) }, 0));
		g2.draw(rect);
		handleHandler.paintSelectionHandles(g2, view.getModel(), transformation.getScaleX());

		g2.setStroke(strokeBackup);

		g2.setTransform(def);
	}

	/** @pdGenerated default parent getter */
	public GraphDocumentView getView() {
		return view;
	}

	/**
	 * @pdGenerated default parent setter
	 * @param newGraphDocumentView
	 */
	public void setView(GraphDocumentView newGraphDocumentView) {
		this.view = newGraphDocumentView;
	}

	public GraphController getController() {
		return controller;
	}

	public AffineTransform getTransformation() {
		return transformation;
	}

	public void setTransformation(AffineTransform transformation) {
		this.transformation = transformation;
	}

	public void setSelectionRectangle(Rectangle2D rect) {
		this.rect = rect;
	}

	public SelectionHandleHandler getHandleHandler() {
		return handleHandler;
	}

	public void setHandleHandler(SelectionHandleHandler handleHandler) {
		this.handleHandler = handleHandler;
	}

	public Handle getSelectedHandle() {
		return selectedHandle;
	}

	public void setSelectedHandle(Handle selectedHandle) {
		this.selectedHandle = selectedHandle;
	}

	/**
	 * Metoda proverava da li je selektovan tačno jedan element i, ako
	 * jeste, vraća ga.
	 */
	public GraphElement getSingleSelectedElement() {
		if (view.getModel().getSelectedElements().size() == 1)
			return view.getModel().getSelectedElements().get(0);
		return null;
	}

	/**
	 * Na osnovu hendla iznad koga se nalazi mis postavlja kursor
	 */
	public void setMouseCursor(Point2D point) {
		for (GraphElement selectedElement : getView().getModel().getSelectedElements())
			if (selectedElement != null && !(selectedElement instanceof Link)) {
				Handle handle = handleHandler.getHandleForPoint(selectedElement, point,
						transformation.getScaleX());
				if (handle != null) {
					Cursor cursor = null;

					switch (handle) {
					case North:
						cursor = Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
						break;
					case South:
						cursor = Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR);
						break;
					case East:
						cursor = Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
						break;
					case West:
						cursor = Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR);
						break;
					case SouthEast:
						cursor = Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR);
						break;
					case NorthWest:
						cursor = Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR);
						break;
					case SouthWest:
						cursor = Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR);
						break;
					case NorthEast:
						cursor = Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR);
						break;
					case NorthEastEast:
						Toolkit toolkit = Toolkit.getDefaultToolkit();
						Image image = toolkit.getImage("resources/icons/sub_black_rotate_cw.png");
						Point hotSpot = new Point(15, 15);
						cursor = toolkit.createCustomCursor(image, hotSpot, "CursorProba1");
						break;
					}
					setCursor(cursor);
					return;
				} else
					setCursor(Cursor.getDefaultCursor());
			}
	}

	/**
	 * Transformacija tacke iz fizickog u logicki koordinatni prostor
	 */
	public void pointToUserSpace(Point2D deviceSpace) {
		try {
			transformation.inverseTransform(deviceSpace, deviceSpace);
		} catch (NoninvertibleTransformException e) {
			e.printStackTrace();
		}
	}

	public class GraphController implements MouseListener, MouseMotionListener, MouseWheelListener,
			KeyListener, WindowFocusListener, WindowStateListener, WindowListener {
		private State currentState;

		public GraphController() {
		}

		/** @param e */
		public void mouseClicked(MouseEvent e) {	
			currentState.mouseClicked(e);
		}

		/** @param e */
		public void mousePressed(MouseEvent e) {
			if(e.getButton() == MouseEvent.BUTTON3) {
				GraphDocumentView activeDocument=Workspace.getInstance().getActiveDocument();
				if(activeDocument!=null){
					State activeState=((GraphWorkArea)activeDocument.getWorkArea()).getController().getCurrentState();
					if(!(activeState instanceof ActiveConnecting)&&
							!(activeState instanceof RotateState)&&
							!(activeState instanceof ResizeState)){
						GToolbar.getInstance().getSelect().doClick();
					}
				}
			}
			
			GraphWorkArea.this.requestFocusInWindow();
			currentState.mousePressed(e);
		}

		/** @param e */
		public void mouseReleased(MouseEvent e) {
			currentState.mouseReleased(e);
		}

		/** @param e */
		public void mouseDragged(MouseEvent e) {

			// autoscroll
			Point2D position = e.getPoint();
			if(position.getX()<=1){
				autoScroll(0);
			}
			else if(position.getY()>=getSize().getHeight()){
				autoScroll(1);
			}
			else if(position.getX()>=getSize().getWidth()){
				autoScroll(2);
			}
			else if(position.getY()<=1){
				autoScroll(3);
			}

			currentState.mouseDragged(e);
		}

		/** @param e */
		public void mouseMoved(MouseEvent e) {
			currentState.mouseMoved(e);
		}

		/** @param e */
		public void mouseWheelMoved(MouseWheelEvent e) {
			// currentState.mouseWheelMoved(e);

			if ((e.getModifiers() & MouseWheelEvent.CTRL_MASK) != 0) { // Ako
																		// pritisnut
																		// Ctrl
				// Radimo zoom u tački (diskretni zoom)
				// Prvo je potrebno da odredimo novo skaliranje
				double newScaling = scaling;
				if (e.getWheelRotation() < 0)

					newScaling *= -(double) e.getWheelRotation() * view.getZoomFactor();

				else {

					if (e.getWheelRotation() != 0) {

						newScaling /= (double) e.getWheelRotation() * view.getZoomFactor();

					}

				}
				// Zatim je potrebno da skaliranje održimo u intervalu
				// [0.2, 2]
				if (newScaling < 0.2) {
					newScaling = 0.2;

				} else if (newScaling > 2) {
					newScaling = 2;
				}

				/*
				 * newScaling je novi parametar skaliranja (članovi
				 * m00 i m11 transformacione matrice) Prilikom skaliranja dolazi
				 * do pomeranja userspace koordinata na kojima se nalazi
				 * pokazivač miša. Da bi dobili ispravan Point
				 * zoom moramo izvršiti translaciju tako da
				 * poništimo "smicanje" usled skaliranja tj. moramo
				 * postići da se userspace koordinate miša ne
				 * promene.
				 */

				Point2D oldPosition = e.getPoint();
				pointToUserSpace(oldPosition);

				scaling = newScaling;
				setupTransformation();

				Point2D newPosition = e.getPoint();
				pointToUserSpace(newPosition);

				translateX += newPosition.getX() - oldPosition.getX();
				translateY += newPosition.getY() - oldPosition.getY();

			} else if ((e.getModifiers() & MouseWheelEvent.SHIFT_MASK) != 0) { // Ako
																				// je
																				// pritisnut
																				// Shift
				int valueScroll = scrollHorizontal.getValue();
				if(((valueScroll >= scrollHorizontal.getMinimum()) &&(e.getWheelRotation() > 0)) || ((e.getWheelRotation() < 0)&& (valueScroll <= scrollHorizontal.getMaximum()))){										
					scrollHorizontal.setValue(scrollHorizontal.getValue()+scrollHorizontal.getUnitIncrement()*e.getWheelRotation());
					valueScroll = scrollHorizontal.getValue();
					hScrollValue = valueScroll;
				}

			} else { // u ostalim slučajevima vršimo
						// skrolovanje po Y osi
				int valueScroll = scrollVertical.getValue();
				if(((valueScroll >= scrollVertical.getMinimum()) && (e.getWheelRotation() > 0)) || ((e.getWheelRotation() < 0) && (valueScroll <= scrollVertical.getMaximum()))){					
					scrollVertical.setValue(scrollVertical.getValue()+scrollVertical.getUnitIncrement()*e.getWheelRotation());
					valueScroll = scrollVertical.getValue();
					vScrollValue = valueScroll;
				}

			}

			setupTransformation();
			repaint();
		}

		/** @param e */
		public void keyPressed(KeyEvent e) {
			currentState.keyPressed(e);
		}

		/** @param e */
		public void keyReleased(KeyEvent e) {
			currentState.keyReleased(e);
		}

		/** @param e */
		public void keyTyped(KeyEvent e) {
			currentState.keyTyped(e);
		}

		/** @param e */
		public void windowGainedFocus(WindowEvent e) {
			currentState.windowGainedFocus(e);
		}

		/** @param e */
		public void windowLostFocus(WindowEvent e) {
			currentState.windowLostFocus(e);
		}

		@Override
		public void windowOpened(WindowEvent e) {
			currentState.windowOpened(e);
		}

		@Override
		public void windowClosing(WindowEvent e) {
			currentState.windowClosing(e);

		}

		@Override
		public void windowClosed(WindowEvent e) {
			currentState.windowClosed(e);
		}

		@Override
		public void windowIconified(WindowEvent e) {
			currentState.windowIconified(e);
		}

		@Override
		public void windowDeiconified(WindowEvent e) {
			currentState.windowDeiconified(e);
		}

		@Override
		public void windowActivated(WindowEvent e) {
			currentState.windowActivated(e);
		}

		@Override
		public void windowDeactivated(WindowEvent e) {
			currentState.windowDeactivated(e);
		}

		@Override
		public void windowStateChanged(WindowEvent e) {
			currentState.windowStateChanged(e);
		}

		@Override
		public void mouseEntered(MouseEvent e) {
			currentState.mouseEntered(e);
		}

		@Override
		public void mouseExited(MouseEvent e) {
			currentState.mouseExited(e);
		}

		public State getCurrentState() {
			return currentState;
		}

		public void setCurrentState(State currentState) {
			this.currentState = currentState;
		}

	}

	@Override
	public void adjustmentValueChanged(AdjustmentEvent e) {
		// Nakon određivanja vrednosti translateX i translateY
		// potrebno je
		// setovati novu transformaciju
		// uzeti u obzir koji je skrol bar pomeran (horizontalni ili vertikalni)
		// i u zavisnosti od prethodne pozicije datog skrol bara
		// i trenutnog skaliranja izvrsiti transformaciju translacije
		if (e.getAdjustable().getOrientation() == Adjustable.HORIZONTAL) {

			translateX -= e.getValue() - hScrollValue;
			hScrollValue = e.getValue();
			setupTransformation();
			repaint();
		} else {
			translateY -= e.getValue() - vScrollValue;
			vScrollValue = e.getValue();
			setupTransformation();
			repaint();

		}

	}

	/**
	 * Podesava parametre transformacione matrice
	 */
	public void setupTransformation() {
		transformation.setToIdentity();
		// Zumiranje
		transformation.scale(scaling, scaling);
		// Skrolovanje
		transformation.translate(translateX, translateY);

	}

	public JScrollBar getScrollVertical() {
		return scrollVertical;
	}

	public void setScrollVertical(JScrollBar scrollVertical) {
		this.scrollVertical = scrollVertical;
	}

	public JScrollBar getScrollHorizontal() {
		return scrollHorizontal;
	}

	public void setScrollHorizontal(JScrollBar scrollHorizontal) {
		this.scrollHorizontal = scrollHorizontal;
	}


	public int gethScrollValue() {
		return hScrollValue;
	}

	public void sethScrollValue(int hScrollValue) {
		this.hScrollValue = hScrollValue;
	}

	public int getvScrollValue() {
		return vScrollValue;
	}

	public void setvScrollValue(int vScrollValue) {
		this.vScrollValue = vScrollValue;
	}

	protected void drawGrid(Graphics2D graph) {
		Color backupColor = graph.getColor();
		Stroke backupStroke = graph.getStroke();

		graph.setColor(Color.green);
		Dimension2D dim = GMainFrame.getInstance().getGraphDocumentViewDimension(
				getView().getModel());

		// TODO ovaj faktor parametrizovati
		double scallingFactor = 100;
		double diff;
		int i;

		// vertical lines
		diff = (translateX % scallingFactor) * scaling;
		i = 0;
		while (diff < dim.getWidth()) {
			graph.drawLine((int) diff, 0, (int) diff, (int) dim.getHeight());
			i++;
			diff = ((translateX % scallingFactor) + scallingFactor * i) * scaling;
		}

		// horizontal lines
		diff = (translateY % scallingFactor) * scaling;
		i = 0;
		while (diff < dim.getHeight()) {
			graph.drawLine(0, (int) diff, (int) dim.getWidth(), (int) diff);
			i++;
			diff = ((translateY % scallingFactor) + scallingFactor * i) * scaling;
		}

		graph.setStroke(backupStroke);
		graph.setColor(backupColor);
	}
	
	
	/**Metoda radi autoscroll u zavisnosti od smera u koji želimo da radimo scroll
	 * @param direction - smer za autoscroll
	 */
	public void autoScroll(int direction) {
		switch(direction) {
		
			case 0: {
						int valueScroll = scrollHorizontal.getValue();
						if(valueScroll >= scrollHorizontal.getMinimum()) {
							scrollHorizontal.setValue(scrollHorizontal.getValue() - 10);
							valueScroll = scrollHorizontal.getValue();
							hScrollValue = valueScroll;
						}
						break;
			}
			case 1: {
						int valueScroll = scrollVertical.getValue();
						if(valueScroll <= scrollVertical.getMaximum()) {				
							scrollVertical.setValue(scrollVertical.getValue() + 10);
							valueScroll = scrollVertical.getValue();
							vScrollValue = valueScroll;
						}
						break;
			}
			case 2: {
						int valueScroll = scrollHorizontal.getValue();
						if(valueScroll <= scrollHorizontal.getMaximum()) {				
							scrollHorizontal.setValue(scrollHorizontal.getValue() + 10);
							valueScroll = scrollHorizontal.getValue();
							hScrollValue = valueScroll;
						}
						break;				
			}
			case 3: {
						int valueScroll = scrollVertical.getValue();
						if(valueScroll <= scrollVertical.getMaximum()){				
							scrollVertical.setValue(scrollVertical.getValue() - 10);
							valueScroll=scrollVertical.getValue();
							vScrollValue=valueScroll;
						}
						break;
				
			}
		}
	}
	
	@Override
	public int print(Graphics graphics, PageFormat pageFormat, int pageIndex)
			throws PrinterException {
		if (pageIndex > 0) {
	          	return(NO_SUCH_PAGE);
	    } else {
	    		Graphics2D g2d = (Graphics2D) graphics;
	    		scrollVertical.setVisible(false);
	    		scrollHorizontal.setVisible(false);
	    		GToolbar.getInstance().getBestFitZoom().doClick();
	    		if(GMainMenu.getInstance().getCheck().isSelected())
	    		{
	    			GMainMenu.getInstance().getCheck().doClick();
	    		}
	    		g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
	    		g2d.scale(0.7, 0.7);
	    		printAll(g2d);
	    		scrollVertical.setVisible(true);
	    		scrollHorizontal.setVisible(true);
	    		return(PAGE_EXISTS);
	    }
	}
}